using System;
using System.Collections.Generic;
using System.Text;
using System.IO.Ports;
using System.Text.RegularExpressions;
using System.ComponentModel;

namespace TfsAlert.Core
{

    #region DataReadEventArgs Definition

    public class DataReadEventArgs : EventArgs
    {
        private string _msg;
        private string _dataReceived;

        public DataReadEventArgs( string dataReceived, string msg )
        {
            _msg = msg;
            _dataReceived = dataReceived;
        }
        public string Message
        {
            get { return _msg; }
        }
        public string DataReceived
        {
            get { return _dataReceived; }
        }
    }

    #endregion

    public static class SerialDefaults
    {
        public const string PORT = "COM1";
        public const int BAUD = 9600;
        public const Parity PARITY = Parity.None;
        public const int DATABITS = 8;
        public const StopBits STOPBITS = StopBits.One;
        public const Handshake HANDSHAKE = Handshake.None;
        public const string NEWLINE = "\r\n";
        public const string PORT_PATTERN = @"COM\d+";
    }

    public static class RelayDefaults
    {
        public const char OK_RESPONSE = '#';
        public const char TURN_RELAY_ON = 'N';
        public const char SET_RELAY_DIRECT = 'R';
        public const char TOGGLE_RELAY = 'T';
        public const char GET_RELAY_STATUS = 'S';
        public const char TURN_RELAY_OFF = 'F';
        public const string STATUS_OK = "OK";

        public const int RELAY_NO_MIN = 1;
        public const int RELAY_NO_MAX = 8;
        public const int ALL_RELAYS_CODE = 0;

    }

    /// <summary>
    /// This class creates a Serial Communications Port object for passing data through any of the existing "COM" ports
    /// </summary>
    public class RelayIO
    {
        private SerialPort _serialPort = null;

        private int _baudRate;
        private Parity _parity;
        private int _dataBits;
        private StopBits _stopBits;
        private string _portName;
        private string _dataSent;
        private string _dataReceived;
        private Handshake _handshake;
        private string _newLine;

        /// <summary>
        /// Delegate for the DataRead event
        /// </summary>
        /// <param name="sender">Object that fired the event</param>
        /// <param name="e">Event specific parameters</param>
        public delegate void DataReadEventHandler( object sender, DataReadEventArgs e );

        /// <summary>
        /// Event fired when data is read in from the port
        /// </summary>
        public event DataReadEventHandler DataReadEvent = delegate { };


        /// <summary>
        /// Constructor using all of the default values
        /// </summary>
        public RelayIO()
            : this( SerialDefaults.PORT, SerialDefaults.BAUD, SerialDefaults.PARITY,
            SerialDefaults.DATABITS, SerialDefaults.STOPBITS, SerialDefaults.HANDSHAKE )
        {
        }


        /// <summary>
        /// Constructor - initialize all of the communication parameters
        /// </summary>
        /// <param name="portName">Communication port to open "COM1", "COM2", etc. - Default: "COM1"</param>
        /// <param name="baudRate">Communication speed - Default: 9600</param>
        /// <param name="parity">Error checking parity - Default: None</param>
        /// <param name="dataBits">Number of data bits sent per character - Default: 8</param>
        /// <param name="stopBits">Number of stop bits sent - Default: 1</param>
        /// <param name="handshake">Type of handshaking used - Default: None</param>
        public RelayIO( string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Handshake handshake )
        {
            // Set custom values
            _portName = portName;
            _baudRate = baudRate;
            _parity = parity;
            _dataBits = dataBits;
            _stopBits = stopBits;
            _handshake = handshake;
            _newLine = SerialDefaults.NEWLINE;
        }


        /// <summary>
        /// Opens the serial port for communication
        /// </summary>
        public void Open()
        {
            if ( _serialPort == null )
            {
                // Create the port object
                _serialPort = new SerialPort( _portName, _baudRate, _parity, _dataBits, _stopBits );

                // Set the handshake
                _serialPort.Handshake = _handshake;
                // Create the event handler for received data
                _serialPort.DataReceived += new SerialDataReceivedEventHandler( ReadData );
                // Set the value of the end of data value
                _serialPort.NewLine = _newLine;
                // Open the serial port
                _serialPort.Open();
            }
        }


        /// <summary>
        /// If the serial port is open then cycle it closed and open again with current properties.
        /// </summary>
        public void Reset()
        {
            if ( _serialPort != null )
            {
                Close();
                Open();
            }
        }


        /// <summary>
        /// The name of the port using the pattern: 'COM#' where # is the serial port number
        /// </summary>
        public string PortName
        {
            get { return _portName; }
            set
            {
                string tmpPortName = value.ToUpper();

                Regex regEx = new Regex( SerialDefaults.PORT_PATTERN );
                Match regexMatch = regEx.Match( tmpPortName );

                if ( regexMatch.Success )
                {
                    _portName = value;
                }
                else
                {
                    throw new ArgumentException( "PortName must be in the format: 'COM#' where # is the serial port number" );
                }
            }
        }


        /// <summary>
        /// Serial port speed 
        /// Default is 9600
        /// </summary>
        public int BaudRate
        {
            get { return _baudRate; }
            set { _baudRate = value; }
        }


        /// <summary>
        /// Error detection scheme parity
        /// Default is None
        /// </summary>
        public Parity ParityValue
        {
            get { return _parity; }
            set { _parity = value; }
        }


        /// <summary>
        /// Number of bits sent per character
        /// Default is 8
        /// </summary>
        public int DataBits
        {
            get { return _dataBits; }
            set { _dataBits = value; }
        }


        /// <summary>
        /// Stop bits 
        /// Default is 1
        /// </summary>
        public StopBits StopBitsValue
        {
            get { return _stopBits; }
            set { _stopBits = value; }
        }


        /// <summary>
        /// Type of handshake
        /// Default is None
        /// </summary>
        public Handshake HandshakeValue
        {
            get { return _handshake; }

            set { _handshake = value; }
        }


        /// <summary>
        /// Value of the end of data marker for data sent or received through the port
        /// </summary>
        public string NewLine
        {
            get { return _newLine; }
            set { _newLine = value; }
        }


        /// <summary>
        /// Event handler for received data
        /// </summary>
        /// <param name="sender">Object that fired the event</param>
        /// <param name="e">DataReceived arguments</param>
        private void ReadData( object sender, SerialDataReceivedEventArgs e )
        {
            byte [] data = new byte [_serialPort.BytesToRead];

            _serialPort.Read( data, 0, data.Length );

            if ( data.Length < 1 )
            {
                throw new Exception( "No Data Received In Buffer" );
            }

            if ( data [data.Length - 1] == RelayDefaults.OK_RESPONSE )
            {
                System.Text.Encoding enc;

                switch ( data.Length )
                {
                    case 8:
                        enc = System.Text.Encoding.ASCII;
                        _dataReceived = enc.GetString( data, 0, data.Length - 3 );

                        string status = _dataReceived.Substring( _dataReceived.Length - 1 );

                        _dataReceived = _dataReceived.Substring( 0, _dataReceived.Length - 3 );

                        _dataReceived += "-" + status;

                        DataReadEvent( this, new DataReadEventArgs( _dataReceived, RelayDefaults.STATUS_OK ) );
                        break;

                    case 1:
                        enc = System.Text.Encoding.ASCII;
                        _dataReceived = enc.GetString( data, 0, 1 );

                        if ( _dataReceived != "#" )
                        {
                            throw new Exception( "Error: data received identified an invalid status" );
                        }
                        else
                        {
                            DataReadEvent( this, new DataReadEventArgs( _dataReceived, RelayDefaults.STATUS_OK ) );
                        }
                        break;

                    default:
                        enc = System.Text.Encoding.ASCII;
                        _dataReceived = enc.GetString( data, 0, data.Length );

                        DataReadEvent( this, new DataReadEventArgs( _dataReceived, RelayDefaults.STATUS_OK ) );
                        break;
                }
            }
            else if ( data [data.Length - 1] != '\n' )
            {
                throw new Exception( "Corrupt data received" );
            }
        }


        /// <summary>
        /// Close the serial port
        /// </summary>
        public void Close()
        {
            if ( _serialPort != null )
            {
                // Clear out the input and output buffers
                if ( _serialPort.BytesToRead > 0 )
                {
                    _serialPort.DiscardInBuffer();
                }

                if ( _serialPort.BytesToWrite > 0 )
                {
                    _serialPort.DiscardOutBuffer();
                }

                // Close the port
                _serialPort.Close();

                // Release the object resources
                _serialPort = null;
            }
        }


        /// <summary>
        /// Validate the relay number is within the range of 0-8
        /// </summary>
        /// <param name="relayNo">The number of the relay</param>
        private void ValidateRelayNo( int relayNo )
        {
            if ( ( relayNo < RelayDefaults.ALL_RELAYS_CODE ) || ( relayNo > RelayDefaults.RELAY_NO_MAX ) )
            {
                throw new ArgumentException( string.Format( "Relay number must be >= {0} and <= {1}", RelayDefaults.ALL_RELAYS_CODE, RelayDefaults.RELAY_NO_MAX ) );
            }
        }


        /// <summary>
        /// Turn a relay on by passing in the relay number
        /// Pass in a 0 to turn them all on
        /// </summary>
        /// <param name="relayNo">The number of the relay</param>
        public void TurnRelayOn( int relayNo )
        {
            ValidateRelayNo( relayNo );

            WriteData( string.Format( "{0}{1}", RelayDefaults.TURN_RELAY_ON, relayNo ) );
        }


        /// <summary>
        /// Set several relays simultaneously.  
        /// </summary>
        /// <param name="relaysToSet">A string value of all of the relays to set. "1258" will set relays 1, 2, 5, and 8 on. 
        /// It will also force relays 3, 4, 6 & 7 off.</param>
        public void SetRelayDirect( string relaysToSet )
        {
            int relayVals = 0;

            // Extract the digits in the string and combine to form a HEX value
            // This routine does not care if the digits are duplicated
            for ( int i = 0; i < relaysToSet.Length; i++ )
            {
                string relay = relaysToSet.Substring( i, 1 );
                int relayNo = int.Parse( relay );

                ValidateRelayNo( relayNo );

                relayVals |= (int)Math.Pow( 2, ( relayNo - 1 ) );
            }

            // convert the value to a hex string and send it out
            WriteData( string.Format( "{0}{1}", RelayDefaults.SET_RELAY_DIRECT, relayVals.ToString( "X" ) ) );
        }

        /// <summary>
        /// Toggle the value of a relay on or off
        /// </summary>
        /// <param name="relayNo">The number of the relay</param>
        public void ToggleRelay( int relayNo )
        {
            ValidateRelayNo( relayNo );

            WriteData( string.Format( "{0}{1}", RelayDefaults.TOGGLE_RELAY, relayNo ) );
        }

        /// <summary>
        /// Get the status of a relay
        /// </summary>
        /// <param name="relayNo">The number of the relay</param>
        public void GetRelayStatus( int relayNo )
        {
            ValidateRelayNo( relayNo );

            WriteData( string.Format( "{0}{1}", RelayDefaults.GET_RELAY_STATUS, relayNo ) );
        }



        /// <summary>
        /// Turn a relay off by passing in the relay number
        /// Pass in a 0 to turn them all off
        /// </summary>
        /// <param name="relayNo">The number of the relay</param>
        public void TurnRelayOff( int relayNo )
        {
            ValidateRelayNo( relayNo );

            WriteData( string.Format( "{0}{1}", RelayDefaults.TURN_RELAY_OFF, relayNo ) );
        }


        /// <summary>
        /// Write data to the serial port
        /// </summary>
        /// <param name="dataToSend">Data String to send through the port</param>
        public void WriteData( string dataToSend )
        {
            if ( _serialPort != null )
            {
                _dataReceived = string.Empty;

                _dataSent = dataToSend;

                _serialPort.WriteLine( dataToSend );
            }
        }
    }
}
